/*
 * Copyright 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package androidx.camera.video

import android.Manifest
import android.content.Context
import androidx.annotation.CheckResult
import androidx.annotation.RequiresPermission
import androidx.annotation.RestrictTo
import androidx.camera.core.impl.utils.ContextUtil
import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.core.content.PermissionChecker
import androidx.core.util.Consumer
import androidx.core.util.Preconditions
import java.util.concurrent.Executor
import kotlin.coroutines.ContinuationInterceptor
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.currentCoroutineContext

/**
 * A recording that can be started at a future time.
 *
 * A pending recording allows for configuration of a recording before it is started. Once a pending
 * recording is started with [start], any changes to the pending recording will not affect the
 * actual recording; any modifications to the recording will need to occur through the controls of
 * the [Recording] class returned by [start].
 *
 * A pending recording can be created using one of the [Recorder] methods for starting a recording
 * such as [Recorder.prepareRecording].
 *
 * There may be more settings that can only be changed per-recorder instead of per-recording,
 * because it requires expensive operations like reconfiguring the camera. For those settings, use
 * the [Recorder.Builder] methods to configure before creating the [Recorder] instance, then create
 * the pending recording with it.
 */
public class PendingRecording
internal constructor(
    context: Context,
    private val recorder: Recorder,
    private val outputOptions: OutputOptions,
) {
    // Application context is sufficient for all our needs, so store that to avoid leaking
    // unused resources. For attribution, ContextUtil.getApplicationContext() will retain the
    // attribution tag from the original context.
    private val applicationContext: Context = ContextUtil.getApplicationContext(context)
    private var eventListener: Consumer<VideoRecordEvent>? = null
    private var listenerExecutor: Executor? = null
    private var isAudioEnabled: Boolean = false
    private var isAudioInitialMuted: Boolean = false
    private var isPersistent: Boolean = false

    /**
     * Returns an application context which was retrieved from the [Context] used to create this
     * object.
     */
    @JvmName("getApplicationContext")
    internal fun getApplicationContext(): Context = applicationContext

    @JvmName("getRecorder") internal fun getRecorder(): Recorder = recorder

    @JvmName("getOutputOptions") internal fun getOutputOptions(): OutputOptions = outputOptions

    @JvmName("getListenerExecutor") internal fun getListenerExecutor(): Executor? = listenerExecutor

    @JvmName("getEventListener")
    internal fun getEventListener(): Consumer<VideoRecordEvent>? = eventListener

    @JvmName("isAudioEnabled") internal fun isAudioEnabled(): Boolean = isAudioEnabled

    @JvmName("isAudioInitialMuted")
    internal fun isAudioInitialMuted(): Boolean = isAudioInitialMuted

    @JvmName("isPersistent") internal fun isPersistent(): Boolean = isPersistent

    /**
     * Enables audio to be recorded for this recording.
     *
     * This method must be called prior to [start] to enable audio in the recording. If this method
     * is not called, the [Recording] generated by [start] will not contain audio, and
     * [AudioStats.getAudioState] will always return [AudioStats.AUDIO_STATE_DISABLED] for all
     * [RecordingStats] send to the listener set passed to [start].
     *
     * Recording with audio requires the [android.Manifest.permission.RECORD_AUDIO] permission;
     * without it, recording will fail at [start] with an [IllegalStateException].
     *
     * @param initialMuted (Optional) The initial mute state of the recording. Defaults to `false`
     *   (un-muted). After the recording is started, the mute state can be changed by calling
     *   [Recording.mute].
     * @return this pending recording
     * @throws IllegalStateException if the [Recorder] this recording is associated to doesn't
     *   support audio.
     * @throws SecurityException if the [Manifest.permission.RECORD_AUDIO] permission is denied for
     *   the current application.
     */
    @RequiresPermission(Manifest.permission.RECORD_AUDIO)
    @JvmOverloads
    public fun withAudioEnabled(initialMuted: Boolean = false): PendingRecording {
        // Check permissions and throw a security exception if RECORD_AUDIO is not granted.
        if (
            PermissionChecker.checkSelfPermission(
                applicationContext,
                Manifest.permission.RECORD_AUDIO,
            ) == PermissionChecker.PERMISSION_DENIED
        ) {
            throw SecurityException(
                "Attempted to enable audio for recording but application " +
                    "does not have RECORD_AUDIO permission granted."
            )
        }
        Preconditions.checkState(
            recorder.isAudioSupported,
            "The Recorder this recording is " + "associated to doesn't support audio.",
        )
        isAudioEnabled = true
        isAudioInitialMuted = initialMuted
        return this
    }

    /**
     * Configures the recording to be a persistent recording.
     *
     * A persistent recording will only be stopped by explicitly calling [Recording.stop] or
     * [Recording.close] and will ignore events that would normally cause recording to stop, such as
     * lifecycle events or explicit unbinding of a [VideoCapture] use case that the recording's
     * [Recorder] is attached to.
     *
     * Even though lifecycle events or explicit unbinding use cases won't stop a persistent
     * recording, it will still stop the camera from producing data, resulting in the in-progress
     * persistent recording stopping getting data until the camera stream is activated again. For
     * example, when the activity goes into background, the recording will keep waiting for new data
     * to be recorded until the activity is back to foreground.
     *
     * A [Recorder] instance is recommended to be associated with a single [VideoCapture] instance,
     * especially when using persistent recording. Otherwise, there might be unexpected behavior.
     * Any in-progress persistent recording created from the same [Recorder] should be stopped
     * before starting a new recording, even if the [Recorder] is associated with a different
     * [VideoCapture].
     *
     * To switch to a different camera stream while a recording is in progress, first create the
     * recording as persistent recording, then rebind the [VideoCapture] it's associated with to a
     * different camera. The implementation may be like:
     * ```
     * // Prepare the Recorder and VideoCapture, then bind the VideoCapture to the back camera.
     * Recorder recorder = Recorder.Builder().build();
     * VideoCapture videoCapture = VideoCapture.withOutput(recorder);
     * cameraProvider.bindToLifecycle(
     *         lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, videoCapture);
     *
     * // Prepare the persistent recording and start it.
     * Recording recording = recorder
     *         .prepareRecording(context, outputOptions)
     *         .asPersistentRecording()
     *         .start(eventExecutor, eventListener);
     *
     * // Record from the back camera for a period of time.
     *
     * // Rebind the VideoCapture to the front camera.
     * cameraProvider.unbindAll();
     * cameraProvider.bindToLifecycle(
     *         lifecycleOwner, CameraSelector.DEFAULT_FRONT_CAMERA, videoCapture);
     *
     *
     * // Record from the front camera for a period of time.
     *
     * // Stop the recording explicitly.
     * recording.stop();
     * ```
     *
     * The audio data will still be recorded after the [VideoCapture] is unbound.
     * [Pause][Recording.pause] the recording first and [resume][Recording.resume] it later to stop
     * recording audio while rebinding use cases.
     *
     * If the recording is unable to receive data from the new camera, possibly because of
     * incompatible surface combination, an exception will be thrown when binding to lifecycle.
     */
    @ExperimentalPersistentRecording
    public fun asPersistentRecording(): PendingRecording {
        isPersistent = true
        return this
    }

    /**
     * Starts the recording, making it an active recording.
     *
     * Only a single recording can be active at a time, so if another recording is active, this will
     * throw an [IllegalStateException].
     *
     * If there are no errors starting the recording, the returned [Recording] can be used to
     * [pause][Recording.pause], [resume][Recording.resume], or [stop][Recording.stop] the
     * recording.
     *
     * Upon successfully starting the recording, a [VideoRecordEvent.Start] event will be the first
     * event sent to the provided event listener.
     *
     * If errors occur while starting the recording, a [VideoRecordEvent.Finalize] event will be the
     * first event sent to the provided listener, and information about the error can be found in
     * that event's [VideoRecordEvent.Finalize.getError] method. The returned [Recording] will be in
     * a finalized state, and all controls will be no-ops.
     *
     * If the returned [Recording] is garbage collected, the recording will be automatically
     * stopped. A reference to the active recording must be maintained as long as the recording
     * needs to be active. If the recording is garbage collected, the [VideoRecordEvent.Finalize]
     * event will contain error [VideoRecordEvent.Finalize.ERROR_RECORDING_GARBAGE_COLLECTED].
     *
     * The [Recording] will be stopped automatically if the [VideoCapture] its [Recorder] is
     * attached to is unbound unless it's created
     * [as a persistent recording][asPersistentRecording].
     *
     * @param listenerExecutor the executor that the event listener will be run on.
     * @param listener the event listener to handle video record events.
     * @throws IllegalStateException if the associated Recorder currently has an unfinished active
     *   recording.
     */
    @CheckResult
    public fun start(listenerExecutor: Executor, listener: Consumer<VideoRecordEvent>): Recording {
        Preconditions.checkNotNull(listenerExecutor, "Listener Executor can't be null.")
        Preconditions.checkNotNull(listener, "Event listener can't be null")
        this.listenerExecutor = listenerExecutor
        eventListener = listener
        return recorder.start(this)
    }

    /**
     * Starts the recording, making it an active recording.
     *
     * @param listener the event listener to handle video record events.
     * @throws IllegalStateException if the associated Recorder currently has an unfinished active
     *   recording.
     * @see PendingRecording.start
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public suspend fun start(listener: Consumer<VideoRecordEvent>): Recording {
        val callbackExecutor =
            (currentCoroutineContext()[ContinuationInterceptor] as? CoroutineDispatcher)
                ?.asExecutor() ?: CameraXExecutors.directExecutor()
        return start(callbackExecutor, listener)
    }
}
